#!/usr/bin/env python3
"""
ESP32 Camera 图像显示脚本
通过串口接收ESP32发送的JPEG图像数据并显示
"""

import serial
import serial.tools.list_ports
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk, ImageOps
from io import BytesIO
import threading
import time

class ESP32CameraViewer:
    def __init__(self, port=None, baudrate=115200):
        self.port = port
        self.baudrate = baudrate
        self.ser = None
        self.root = tk.Tk()
        self.root.title("ESP32 Camera Viewer")
        self.root.geometry("400x300")
        
        # 创建显示图像的标签
        self.image_label = tk.Label(self.root)
        self.image_label.pack(expand=True, fill=tk.BOTH)
        
        # 状态标签
        self.status_label = tk.Label(self.root, text="未连接", fg="red")
        self.status_label.pack(side=tk.BOTTOM, fill=tk.X)
        
        # 创建串口选择界面
        self.create_port_selection_ui()
        
        # 图像数据缓冲区
        self.image_buffer = bytearray()
        self.receiving_image = False
        self.image_size = 0
        
        # 如果提供了端口，则直接连接
        if self.port:
            self.update_status("准备连接到 " + self.port)
        else:
            self.update_status("请选择串口")
    
    def create_port_selection_ui(self):
        # 串口选择框架
        port_frame = tk.Frame(self.root)
        port_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=5)
        
        # 获取可用串口列表
        ports = self.get_available_ports()
        
        # 串口选择下拉框
        self.port_var = tk.StringVar()
        if ports:
            self.port_var.set(ports[0] if not self.port else self.port)
        
        tk.Label(port_frame, text="选择串口:").pack(side=tk.LEFT)
        self.port_combo = ttk.Combobox(port_frame, textvariable=self.port_var, values=ports, state="readonly")
        self.port_combo.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        
        # 刷新串口按钮
        refresh_btn = tk.Button(port_frame, text="刷新", command=self.refresh_ports)
        refresh_btn.pack(side=tk.LEFT, padx=(0, 5))
        
        # 连接按钮
        self.connect_button = tk.Button(port_frame, text="连接", command=self.toggle_connection)
        self.connect_button.pack(side=tk.LEFT)
    
    def get_available_ports(self):
        """获取可用的串口列表"""
        ports = serial.tools.list_ports.comports()
        return [port.device for port in ports]
    
    def refresh_ports(self):
        """刷新串口列表"""
        ports = self.get_available_ports()
        self.port_combo['values'] = ports
        if ports and not self.port_var.get() in ports:
            self.port_var.set(ports[0])
    
    def toggle_connection(self):
        if self.ser and self.ser.is_open:
            self.disconnect()
        else:
            self.port = self.port_var.get()
            self.connect()
    
    def connect(self):
        try:
            # 如果之前连接失败，确保旧的串口对象被关闭
            if self.ser:
                self.ser.close()
            self.ser = serial.Serial(self.port, self.baudrate, timeout=1)
            self.update_status("已连接到 " + self.port)
            self.connect_button.config(text="断开连接")
            
            # 启动接收线程
            self.receive_thread = threading.Thread(target=self.receive_data, daemon=True)
            self.receive_thread.start()
            
        except Exception as e:
            self.update_status("连接失败: " + str(e), "red")
            self.ser = None  # 确保失败后串口对象为 None
    
    def disconnect(self):
        if self.ser and self.ser.is_open:
            self.ser.close()
            self.update_status("已断开连接", "red")
            self.connect_button.config(text="连接")
    
    def update_status(self, text, color="black"):
        self.status_label.config(text=text, fg=color)
        self.root.update_idletasks()
    
    def receive_data(self):
        """接收串口数据的线程函数"""
        while self.ser and self.ser.is_open:
            try:
                # 设置最大等待时间为 1 秒
                if self.ser.in_waiting > 0:
                    data = self.ser.read(self.ser.in_waiting)
                    self.process_data(data)
                time.sleep(0.01)  # 短暂休眠以减少CPU使用率
            except serial.SerialTimeoutException:
                self.update_status("串口读取超时", "red")
                break
            except Exception as e:
                self.update_status("读取数据错误: " + str(e), "red")
                break
    
    def process_data(self, data):
        """处理接收到的数据"""
        try:
            # 查找JPEG开始和结束标记，用于图像显示
            start_index = data.find(b'\xff\xd8')
            end_index = data.find(b'\xff\xd9')
            
            # 总是尝试解析文本信息，确保关键结果信息能被输出
            try:
                text_data = data.decode('utf-8', errors='ignore')
                # 特别保留RESULT开头的关键信息
                lines = text_data.split('\n')
                for line in lines:
                    if line.strip() and (line.startswith('RESULT') or not self.receiving_image):
                        # 过滤掉可能的二进制字符，只保留可打印字符和换行符等
                        printable_line = ''.join(char if char.isprintable() or char in ['\n', '\r', '\t'] else '' for char in line)
                        if printable_line.strip():
                            print(printable_line)
            except Exception as decode_error:
                # 解码错误时忽略，不打印图像数据造成的乱码
                pass
            
            if not self.receiving_image:
                # 查找JPEG开始标记
                if start_index != -1:
                    self.receiving_image = True
                    self.image_buffer = bytearray()
                    self.update_status("开始接收图像...", "blue")
                    # 从开始标记处继续处理
                    self.image_buffer.extend(data[start_index:])
                    
                    # 检查在同一批数据中是否也包含结束标记
                    if end_index != -1 and end_index > start_index:
                        self.receiving_image = False
                        self.image_buffer = self.image_buffer[:end_index+2]  # 包含结束标记
                        self.update_status("图像接收完成，正在显示...", "green")
                        self.display_image()
            else:
                # 正在接收图像数据
                self.image_buffer.extend(data)
                
                # 查找JPEG结束标记
                if end_index != -1:
                    # 包含结束标记
                    self.image_buffer = self.image_buffer[:len(self.image_buffer) - len(data) + end_index + 2]
                    self.receiving_image = False
                    self.update_status("图像接收完成，正在显示...", "green")
                    self.display_image()
                
        except Exception as e:
            print(f"处理数据时出错: {e}")
    
    def display_image(self):
        """显示接收到的图像"""
        try:
            # 创建BytesIO对象用于PIL处理
            image_stream = BytesIO(self.image_buffer)
            image = Image.open(image_stream)
            
            # 调整图像大小以适应窗口
            window_width = self.root.winfo_width() or 400
            window_height = self.root.winfo_height() or 300
            
            # 计算新尺寸保持宽高比
            img_width, img_height = image.size
            ratio = min(window_width/img_width, window_height/img_height)
            new_width = int(img_width * ratio)
            new_height = int(img_height * ratio)
            
            # 调整图像大小
            image = image.resize((new_width, new_height), Image.LANCZOS)
            
            # 转换为Tkinter兼容的格式
            photo = ImageTk.PhotoImage(image)
            
            # 更新标签中的图像
            self.image_label.config(image=photo)
            self.image_label.image = photo  # 保持引用以防止被垃圾回收
            
            self.update_status("图像显示成功", "green")
            
        except Exception as e:
            self.update_status("显示图像失败: " + str(e), "red")
            print(f"显示图像时出错: {e}")
    
    def run(self):
        """运行GUI主循环"""
        self.root.mainloop()

def main():
    # 创建并运行相机查看器
    viewer = ESP32CameraViewer()
    viewer.run()

if __name__ == "__main__":
    main()